High Level Design

Android Architecture from the bird's-eye view

Understanding how to structure large-scale Android applications — from UI to data, from single-screen flows to multi-module systems.

Explore Architecture See Examples

01 — Overview

The Android Architecture Stack

Modern Android apps are structured in horizontal layers. Each layer has a clear responsibility and communicates only with adjacent layers.

Layered Architecture — Top to Bottom
UI Layer
Activity / Fragment
Composable (Jetpack)
ViewModel
Domain
Use Cases
Entities / Models
Repository Interfaces
Data Layer
Repository Impl
Remote (Retrofit)
Local (Room)
UI Layer
Domain Layer
Data Layer
Core Principle: Each layer depends only on the layer below it. The UI knows about ViewModels; ViewModels know about UseCases; UseCases depend on Repository interfaces — never on implementations. This keeps the code testable and maintainable.
UI Layer
Activity Fragment Compose ViewModel
Renders the app's state on screen and handles user input. ViewModels expose state via StateFlow or LiveData. The UI observes and reacts — it never holds business logic. Activities and Fragments (or Composables) are kept thin.
Domain Layer
UseCase Entity Mapper
Optional but recommended for complex apps. Contains pure business logic. Use cases are single-action classes like GetUserProfileUseCase. This layer is pure Kotlin — no Android imports — making it trivially testable.
Data Layer
Repository DataSource Room Retrofit
Owns all data operations. Repositories coordinate between remote (API) and local (Room/DataStore) sources. They expose clean, platform-agnostic data to the domain layer via Kotlin Flows.

02 — Patterns

Common HLD Patterns

These architectural patterns define how data flows between components and how responsibilities are divided.

🔷
MVVM
Model-View-ViewModel. The ViewModel holds and exposes UI state. The View (Activity/Fragment/Compose) observes and renders. The Model is the data layer. The standard choice for Android.
// ViewModel exposes state
class HomeViewModel : ViewModel() {
  val uiState = MutableStateFlow(
    HomeUiState.Loading
  )
}
🔁
MVI
Model-View-Intent. Unidirectional data flow. The user fires Intents → ViewModel reduces them into State → View renders. Deterministic and highly testable. Great for complex UIs.
// Sealed intent class
sealed class SearchIntent {
  data class Query(
    val text: String
  ) : SearchIntent()
}
🧅
Clean Architecture
Concentric circles of dependency inversion. Outer layers depend on inner ones, never the reverse. Enforces testability and separation of concerns across large teams.
// Domain UseCase — pure Kotlin
class GetNewsUseCase(
  private val repo: NewsRepository
) {
  operator fun invoke() = repo.getNews()
}
📦
Multi-Module
Each feature lives in its own Gradle module. Speeds up builds, enforces boundaries, and allows feature teams to work independently. Typical modules: app, core, feature:home, feature:profile.
// settings.gradle.kts
include(":app")
include(":core:network")
include(":core:database")
include(":feature:home")
include(":feature:search")
🗄️
Repository Pattern
A single source of truth. The repository decides whether to fetch from the network or serve from cache. Callers don't care — they just request data.
// Single source of truth
fun getUser(id: String) = flow {
  emit(localDb.getUser(id))
  val fresh = api.fetchUser(id)
  localDb.save(fresh)
  emit(fresh)
}
💉
Dependency Injection
Hilt (recommended) or Koin wire up dependencies. Objects don't construct their own dependencies — they declare what they need. Makes testing and swapping implementations trivial.
// Hilt ViewModel injection
@HiltViewModel
class ProfileViewModel @Inject constructor(
  private val useCase: GetProfileUseCase
) : ViewModel()

03 — Components

Key Android Components & Libraries

The Jetpack ecosystem provides battle-tested building blocks for each layer of the architecture.

Component Layer Purpose Replaces
ViewModel UI Holds UI state, survives configuration changes Raw Activity fields
StateFlow / LiveData UI Observable state holder between ViewModel and UI Callbacks, RxJava
Jetpack Compose UI Declarative UI toolkit replacing XML layouts XML + RecyclerView
Navigation Component UI Type-safe navigation graph, backstack management FragmentManager
UseCase Domain Encapsulates a single business operation Fat ViewModel logic
Hilt Domain Compile-time dependency injection via annotations Manual DI, Dagger
Repository Data Coordinates local and remote data sources Direct API calls
Retrofit + OkHttp Data Type-safe HTTP client for REST APIs HttpURLConnection
Room Data SQLite abstraction with compile-time query validation Raw SQLite
DataStore Data Async key-value and proto-based preferences SharedPreferences

Unidirectional Data Flow

User Event tap / scroll
ViewModel processes intent
UseCase business logic
Repository fetch / cache
DataSource API / DB
UI State renders on screen

04 — Real Examples

HLD for a News App

Let's see how all layers and patterns come together in a concrete, real-world example — a news reader application.

PROJECT STRUCTURE
// Gradle module tree
:app ← wires everything together
:core
  :core:network ← Retrofit, OkHttp, interceptors
  :core:database ← Room, DAOs, entities
  :core:domain ← base UseCase, models
  :core:ui ← shared composables, theme
:feature
  :feature:home ← news feed screen
  :feature:detail ← article detail screen
  :feature:search ← search functionality
  :feature:bookmarks ← saved articles
UI LAYER — HomeViewModel.kt
@HiltViewModel
class HomeViewModel @Inject constructor(
  private val getTopHeadlines: GetTopHeadlinesUseCase,
  private val bookmarkArticle: BookmarkArticleUseCase
) : ViewModel() {

  private val _uiState = MutableStateFlow<HomeUiState>(HomeUiState.Loading)
  val uiState: StateFlow<HomeUiState> = _uiState.asStateFlow()

  init {
    loadHeadlines()
  }

  fun onIntent(intent: HomeIntent) = when (intent) {
    is HomeIntent.Refresh -> loadHeadlines()
    is HomeIntent.Bookmark -> bookmark(intent.article)
  }
}
DATA LAYER — NewsRepositoryImpl.kt
class NewsRepositoryImpl @Inject constructor(
  private val remoteDataSource: NewsRemoteDataSource,
  private val localDataSource: NewsLocalDataSource
) : NewsRepository {

  override fun getTopHeadlines(): Flow<List<Article>> = flow {
    // 1. Emit cached data immediately
    emit(localDataSource.getArticles())

    // 2. Fetch fresh data from network
    val fresh = remoteDataSource.fetchHeadlines()

    // 3. Persist and emit updated list
    localDataSource.saveArticles(fresh)
    emit(fresh)
  }
}
NAVIGATION — AppNavGraph.kt (Compose)
@Composable
fun AppNavGraph(navController: NavHostController) {
  NavHost(navController, startDestination = "home") {

    composable("home") {
      HomeScreen(onArticleClick = { id ->
        navController.navigate("detail/$id")
      })
    }

    composable("detail/{articleId}") { backStack ->
      val id = backStack.arguments?.getString("articleId")
      ArticleDetailScreen(articleId = id)
    }
  }
}
Pro Tip: For interview HLD rounds, always start with the three layers, then discuss data flow direction, then choose patterns (MVVM vs MVI), then list libraries. Finally, address scalability concerns like multi-module or modularization strategy.

05 — Scalability

Scaling for Large Teams

When the codebase grows beyond a few screens, these strategies keep builds fast and teams independent.

Feature Flags
Ship code behind flags to decouple deployment from release. Features can be toggled remotely via Firebase Remote Config or custom flagging systems without an app update.
Offline-First
Design repositories to serve cached data first. Use Room as the source of truth, syncing with the server in the background. Users always see something, even on poor connections.
Paging 3
Jetpack Paging handles loading large datasets incrementally. Integrates natively with Room and Retrofit, and plugs directly into Compose LazyColumn or RecyclerView.
WorkManager
Guaranteed background work that respects Doze mode and battery constraints. Ideal for sync jobs, data uploads, or scheduled cleanup tasks that must survive app death.